﻿// 文件使用MiniExcel源码， 使用时需遵循其 Apache License 2.0
// 修改摘要通知： 删除异步、Task等net2.0中不支持的内容，替换原先C#自带的zip处理方式，改用yacomp 进行解压缩。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using ExcelConfig.Excel.SaveByTemplate;
using ExcelConfig.helpers;
using ExcelConfig.OpenXml;
using ExcelConfig.OpenXml.Models;
using ExcelConfig.Picture;
using ExcelConfig.Utils;
using ExcelConfig.Zip;

namespace ExcelConfig
{
    internal static partial class MiniExcel
    {
        internal static void AddPicture(string path, params MiniExcelPicture[] images)
        {
            using (var stream = File.Open(path,FileMode.OpenOrCreate))
                MiniExcelPictureImplement.AddPicture(stream, images);
        }
        
        internal static void AddPicture(Stream excelStream, params MiniExcelPicture[] images)
        {
            MiniExcelPictureImplement.AddPicture(excelStream, images);
        }
        
        internal static MiniExcelDataReader GetReader(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
        {
            var stream = FileHelper.OpenSharedRead(path);
            return new MiniExcelDataReader(stream, useHeaderRow, sheetName, excelType, startCell, configuration);
        }

        internal static MiniExcelDataReader GetReader(Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
        {
            return new MiniExcelDataReader(stream, useHeaderRow, sheetName, excelType, startCell, configuration);
        }

        internal static int Insert(string path, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null, bool printHeader = true, bool overwriteSheet = false)
        {
            if (Path.GetExtension(path).ToLowerInvariant() == ".xlsm")
                throw new NotSupportedException("MiniExcel's Insert does not support the .xlsm format");

            if (!File.Exists(path))
            {
                var rowsWritten = SaveAs(path, value, printHeader, sheetName, excelType, configuration);
                if (rowsWritten.Length == 0) return default;
                return rowsWritten[0];
            }

            if (excelType == ExcelType.CSV)
            {
                using (var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan))
                    return Insert(stream, value, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration, printHeader, overwriteSheet);
            }
            else
            {
                using (var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.SequentialScan))
                    return Insert(stream, value, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration, printHeader, overwriteSheet);
            }
        }

        internal static int Insert(Stream stream, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null, bool printHeader = true, bool overwriteSheet = false)
        {
            stream.Seek(0, SeekOrigin.End);
            if (excelType == ExcelType.CSV)
            {
                var newValue = value is IEnumerable || value is IDataReader ? value : new[]{value};
                return ExcelWriterFactory.GetProvider(stream, newValue, sheetName, excelType, configuration, false).Insert(overwriteSheet);
            }
            else
            {
                var configOrDefault = configuration ?? new OpenXmlConfiguration { FastMode = true };
                return ExcelWriterFactory.GetProvider(stream, value, sheetName, excelType, configOrDefault, printHeader).Insert(overwriteSheet);
            }
        }
        internal static int[] SaveAs(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null, bool overwriteFile = false)
        {
            if (Path.GetExtension(path).ToLowerInvariant() == ".xlsm")
                throw new NotSupportedException("MiniExcel's SaveAs does not support the .xlsm format");
            using (var stream = overwriteFile ? File.Create(path) : new FileStream(path, FileMode.CreateNew))
                return SaveAs(stream, value, printHeader, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration);
        }
        internal static void SaveAsOverwrite(string path, IEnumerable<IDictionary<string, object>> value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null)
        {
            if (Path.GetExtension(path).ToLowerInvariant() == ".xlsm")
                throw new NotSupportedException("MiniExcel's SaveAs does not support the .xlsm format");
            if (File.Exists(path))
            {
                using (var mem = new MemoryStream())
                {
                    using (var stream = FileHelper.OpenSharedRead(path))
                        ExcelTemplateFactory.GetProvider(mem, configuration).SaveAsByTemplate(stream, new ReplaceSheetDto { sheet_data = value, SheetName = sheetName });

                    mem.Position = 0;
                    using (var stream = File.Create(path))
                        helper.ReadAllToMemory(mem, stream);
                }
            }
            else
            {
                using (var stream = new FileStream(path, FileMode.CreateNew))
                    SaveAs(stream, value, true, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration);
            }
        }

        internal static Stream SaveAsOverwrite_xlsx_stream(Stream stream, IEnumerable<IDictionary<string, object>> value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null)
        {
            var mem = new MemoryStream();
            ExcelTemplateFactory.GetProvider(mem, configuration).SaveAsByTemplate(stream, new ReplaceSheetDto { sheet_data = value, SheetName = sheetName });
            mem.Position = 0;
            return mem;
        }

        internal static int[] SaveAs(Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null)
        {
            return ExcelWriterFactory.GetProvider(stream, value, sheetName, excelType, configuration, printHeader).SaveAs();
        }

        internal static IEnumerable<T> Query<T>(string path, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null, bool hasHeader = true) where T : class, new()
        {
            using (var stream = FileHelper.OpenSharedRead(path))
                foreach (var item in Query<T>(stream, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), startCell, configuration, hasHeader))
                    yield return item; //Foreach yield return twice reason : https://stackoverflow.com/questions/66791982/ienumerable-extract-code-lazy-loading-show-stream-was-not-readable
        }

        internal static IEnumerable<T> Query<T>(Stream stream, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null, bool hasHeader = true) where T : class, new()
        {
            using (var excelReader = ExcelReaderFactory.GetProvider(stream, ExcelTypeHelper.GetExcelType(stream, excelType), configuration))
                foreach (var item in excelReader.Query<T>(sheetName, startCell, hasHeader))
                    yield return item;
        }
        internal static IEnumerable<IDictionary<string, object>> Query(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
        {
            using (var stream = FileHelper.OpenSharedRead(path))
                foreach (var item in Query(stream, useHeaderRow, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), startCell, configuration))
                    yield return item;
        }
        internal static IEnumerable<IDictionary<string, object>> Query_xlsx_stream(Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
        {
            foreach (var item in Query(stream, useHeaderRow, sheetName, ExcelType.XLSX, startCell, configuration))
                yield return item;
        }
        internal static IEnumerable<IDictionary<string, object>> Query(Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
        {
            using (var excelReader = ExcelReaderFactory.GetProvider(stream, ExcelTypeHelper.GetExcelType(stream, excelType), configuration))
                foreach (var item in excelReader.Query(useHeaderRow, sheetName, startCell))
                    yield return item;
        }

        #region range

        /// <summary>
        /// Extract the given range。 Only uppercase letters are effective。
        /// e.g.
        ///     MiniExcel.QueryRange(path, startCell: "A2", endCell: "C3")
        ///     A2 represents the second row of column A, C3 represents the third row of column C
        ///     If you don't want to restrict rows, just don't include numbers
        /// </summary>
        /// <param name="path"></param>
        /// <param name="useHeaderRow"></param>
        /// <param name="sheetName"></param>
        /// <param name="excelType"></param>
        /// <param name="startCell">top left corner</param>
        /// <param name="endCell">lower right corner</param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        internal static IEnumerable<IDictionary<string, object>> QueryRange(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "a1", string endCell = "", IConfiguration configuration = null)
        {
            using (var stream = FileHelper.OpenSharedRead(path))
                foreach (var item in QueryRange(stream, useHeaderRow, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), startCell == "" ? "a1" : startCell, endCell, configuration))
                    yield return item;
        }

        internal static IEnumerable<IDictionary<string, object>> QueryRange(Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "a1", string endCell = "", IConfiguration configuration = null)
        {
            using (var excelReader = ExcelReaderFactory.GetProvider(stream, ExcelTypeHelper.GetExcelType(stream, excelType), configuration))
                foreach (var item in excelReader.QueryRange(useHeaderRow, sheetName, startCell == "" ? "a1" : startCell, endCell))
                    yield return item;
        }

        #endregion range

        internal static void SaveAsByTemplate(string path, string templatePath, object value, IConfiguration configuration = null)
        {
            using (var stream = File.Create(path))
                SaveAsByTemplate(stream, templatePath, value, configuration);
        }

        internal static void SaveAsByTemplate(string path, byte[] templateBytes, object value, IConfiguration configuration = null)
        {
            using (var stream = File.Create(path))
                SaveAsByTemplate(stream, templateBytes, value, configuration);
        }

        internal static void SaveAsByTemplate(Stream stream, string templatePath, object value, IConfiguration configuration = null)
        {
            ExcelTemplateFactory.GetProvider(stream, configuration).SaveAsByTemplate(templatePath, value);
        }
        internal static void SaveAsByTemplate(Stream stream, Stream template_stream, object value, IConfiguration configuration = null)
        {
            ExcelTemplateFactory.GetProvider(stream, configuration).SaveAsByTemplate(template_stream, value);
        }

        internal static void SaveAsByTemplate(Stream stream, byte[] templateBytes, object value, IConfiguration configuration = null)
        {
            ExcelTemplateFactory.GetProvider(stream, configuration).SaveAsByTemplate(templateBytes, value);
        }

        #region MergeCells

        internal static void MergeSameCells(string mergedFilePath, string path, ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null)
        {
            using (var stream = File.Create(mergedFilePath))
                MergeSameCells(stream, path, excelType, configuration);
        }

        internal static void MergeSameCells(Stream stream, string path, ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null)
        {
            ExcelTemplateFactory.GetProvider(stream, configuration, excelType).MergeSameCells(path);
        }

        internal static void MergeSameCells(Stream stream, byte[] filePath, ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null)
        {
            ExcelTemplateFactory.GetProvider(stream, configuration, excelType).MergeSameCells(filePath);
        }

        #endregion

        /// <summary>
        /// QueryAsDataTable is not recommended, because it'll load all data into memory.
        /// </summary>
        [Obsolete("QueryAsDataTable is not recommended, because it'll load all data into memory.")]
        internal static DataTable QueryAsDataTable(string path, bool useHeaderRow = true, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
        {
            using (var stream = FileHelper.OpenSharedRead(path))
            {
                return QueryAsDataTable(stream, useHeaderRow, sheetName, excelType: ExcelTypeHelper.GetExcelType(path, excelType), startCell, configuration);
            }
        }

        internal static DataTable QueryAsDataTable(Stream stream, bool useHeaderRow = true, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
        {
            if (sheetName == null && excelType != ExcelType.CSV) /*Issue #279*/
                sheetName = GetSheetNames(stream, configuration as OpenXmlConfiguration)[0];

            var dt = new DataTable(sheetName);
            var first = true;
            var rows = ExcelReaderFactory.GetProvider(stream, ExcelTypeHelper.GetExcelType(stream, excelType), configuration).Query(false, sheetName, startCell);

            var columnDict = new Dictionary<string, string>();
            foreach (IDictionary<string, object> row in rows)
            {
                if (first)
                {
                    foreach (var entry in row)
                    {
                        var columnName = useHeaderRow ? entry.Value?.ToString() : entry.Key;
                        if (!string.IsNullOrEmpty(columnName.Trim())) // avoid #298 : Column '' does not belong to table
                        {
                            var column = new DataColumn(columnName, typeof(object)) { Caption = columnName };
                            dt.Columns.Add(column);
                            columnDict.Add(entry.Key, columnName);//same column name throw exception???
                        }
                    }
                    dt.BeginLoadData();
                    first = false;
                    if (useHeaderRow)
                    {
                        continue;
                    }
                }

                var newRow = dt.NewRow();
                foreach (var entry in columnDict)
                {
                    newRow[entry.Value] = row[entry.Key]; //TODO: optimize not using string key
                }

                dt.Rows.Add(newRow);
            }

            dt.EndLoadData();
            return dt;
        }

        internal static List<string> GetSheetNames(string path, OpenXmlConfiguration config = null)
        {
            using (var stream = FileHelper.OpenSharedRead(path)) return GetSheetNames(stream, config);
        }
        internal static List<string> GetSheetNames_xlsx_stream(Stream stream, OpenXmlConfiguration config = null)
        {
            return GetSheetNames(stream, config);
        }
        internal static IEnumerable<string> GetSheetNames_enum(string path, OpenXmlConfiguration config = null)
        {
            using (var stream = FileHelper.OpenSharedRead(path)) foreach (var name in GetSheetNames_enum(stream, false, config)) yield return name;
        }

        internal static List<string> GetSheetNames(Stream stream, OpenXmlConfiguration config = null)
        {
            config = config ?? OpenXmlConfiguration.DefaultConfig;

            using (var archive = new ExcelOpenXmlZip(stream))
            {
                var sheetNames = new List<string>();
                foreach (var rel in new ExcelOpenXmlSheetReader(stream, config).GetWorkbookRels(archive.storage)) sheetNames.Add(rel.Name);
                return sheetNames;
            }
        }
        internal static IEnumerable<string> GetSheetNames_enum(Stream stream, bool leaveopen, OpenXmlConfiguration config = null)
        {
            config = config ?? OpenXmlConfiguration.DefaultConfig;
            using (var archive = new ExcelOpenXmlZip(stream, leaveopen))
            {
                foreach (var rel in new ExcelOpenXmlSheetReader(stream, config).GetWorkbookRels(archive.storage)) yield return rel.Name;
            }
        }

        internal static List<SheetInfo> GetSheetInformations(string path, OpenXmlConfiguration config = null)
        {
            using (var stream = FileHelper.OpenSharedRead(path))
                return GetSheetInformations(stream, config);
        }

        internal static List<SheetInfo> GetSheetInformations(Stream stream, OpenXmlConfiguration config = null)
        {
            config = config ?? OpenXmlConfiguration.DefaultConfig;

            using (var archive = new ExcelOpenXmlZip(stream))
            {
                var rlt = new List<SheetInfo>();
                int i = 0;
                foreach (var rel in new ExcelOpenXmlSheetReader(stream, config).GetWorkbookRels(archive.storage)) rlt.Add(rel.ToSheetInfo((uint)i++));
                return rlt;
            }
        }

        internal static ICollection<string> GetColumns(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
        {
            using (var stream = FileHelper.OpenSharedRead(path))
                return GetColumns(stream, useHeaderRow, sheetName, excelType, startCell, configuration);
        }

        internal static ICollection<string> GetColumns(Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
        {
            var query = Query(stream, useHeaderRow, sheetName, excelType, startCell, configuration).GetEnumerator();
            if (query.MoveNext() && query.Current != null) return query.Current.Keys;
            return default;
        }
        
        internal static IList<ExcelRange> GetSheetDimensions(string path)
        {
            using (var stream = FileHelper.OpenSharedRead(path))
                return GetSheetDimensions(stream);
        }

        internal static IList<ExcelRange> GetSheetDimensions(Stream stream)
        {
            return new ExcelOpenXmlSheetReader(stream, null).GetDimensions();
        }
        
        internal static void ConvertCsvToXlsx(string csv, string xlsx)
        {
            using (var csvStream = FileHelper.OpenSharedRead(csv))
            using (var xlsxStream = new FileStream(xlsx, FileMode.CreateNew))
            {
                ConvertCsvToXlsx(csvStream, xlsxStream);
            }
        }

        internal static void ConvertCsvToXlsx(Stream csv, Stream xlsx)
        {
            var value = Query(csv, useHeaderRow: false, excelType: ExcelType.CSV);
            SaveAs(xlsx, value, printHeader: false, excelType: ExcelType.XLSX);
        }

        internal static void ConvertXlsxToCsv(string xlsx, string csv)
        {
            using (var xlsxStream = FileHelper.OpenSharedRead(xlsx))
            using (var csvStream = new FileStream(csv, FileMode.CreateNew))
                ConvertXlsxToCsv(xlsxStream, csvStream);
        }

        internal static void ConvertXlsxToCsv(Stream xlsx, Stream csv)
        {
            var value = Query(xlsx, useHeaderRow: false, excelType: ExcelType.XLSX);
            SaveAs(csv, value, printHeader: false, excelType: ExcelType.CSV);
        }
    }
}